Story3 데이터 송·수신

1 프로토콜 스택에 HTTP 요청 메세지 남기기

이번에는 write() 과정에 대해서 작성할 예정

데이터 보내기

프로토콜 스택은 받은 데이터의 내용에 뭐가 써있는지는 모른다. write()할 때 송신 데이터의 길이는 지정하지만, 바이너리 데이터가 1바이트씩 차례로 나열되어 있다고 인식

데이터 보내는 방식에는 2가지가 있다.

  1. 1바이트씩 또는 1행씩 세분하여 송신 의뢰하기
  2. 데이터를 전부 한꺼번에 송신 의뢰하기

어느 경우든지 한 번의 송신 의뢰에서 건네주는 데이터의 길이는 애플리케이션에 따라 결정되며 프로토콜 스택이 제어할 수 없다.

이런 상황에서 작은 패킷으로 많이 보낼 경우 네트워크 이용 효율 감소하기 때문에 버퍼에 데이터를 저장하고 송수신 동작을 한다.

어디까지 저장하는가에 대한 판단 요소

한 패킷에 저장할 수 있는 데이터의 크기가 판단 요소가 될 수 있다. 프로토콜 스택은 MTU라는 매개변수를 바탕으로 판단한다.

MTU(Maximum Transmission Unit)는 한 패킷으로 운반할 수 있는 디지털 데이터의 최대 길이(이더넷에서는 1500 바이트)

해더를 제외하고 한 개의 패킷으로 운반할 수 있는 데이터의 최대 길이는 MSS(Maximum Segment Size)

또 하나는 타이밍이 될 수 있다. 애플리케이션 송신 속도가 느려진 경우 MSS에 가깝게 데이터 저장하면 시간이 걸려 송신 동작이 지연된다.

이럴 경우에는 프로토콜 스택 내부에 타이머가 있어서 패킷을 송신한다.

둘의 관계는 상관 관계가 있다.

시간을 길게 하면 네트워크 이용 효율 증가, 하지만 송신 동작 지연 우려 시간을 짧게 하면 송신 동작 빠르게 실행, 하지만 네트워크 효율 감소

이 부분은 프로토콜 스택 개발자에 의해 결정됨.

브라우저와 같은 대화형 애플리케이션이 서버에 메세지를 보낼때는 버퍼 머무는 시간 = 지연 시간이므로 바로 송신하는 옵션 지정한다.

2 데이터가 클 때는 분할하여 보낸다

데이터가 큰 경우(MSS의 길이를 초과할 경우) 맨 앞부터 차례대로 MSS 크기에 맞게 분할하고, 분할한 조각을 한 패킷에 넣어 송신.

분할한 조각에 TCP 헤더를 부과하고, 나중에 IP 헤더도 부과하여 송신 동작을 실행

3 ACK 번호를 사용하여 패킷이 도착했는지 확인

데이터 송신 동작은 송신한 패킷이 상대에게 올바르게 도착했는지 확인하고 도착하지 않았으면 다시 송신한다.

이때 도착했는지 확인 방법은 어떻게 될까?

아까 데이터를 분할할때, TCP 담당 부분은 조각이 몇 번째 바이터에 해당하는지 세어둔다. 이 데이터를 TCP 헤더에 기록해두는데, 이것이 시퀀스 번호이다.

데이터 길이 = 패킷 전체 길이 - 헤더 길이이기 때문에 수신측에 데이터 길이를 따로 알리지는 않는다.

따라서 수신측에서는 전 패킷에서 받은 시퀀스 번호 + 데이터 길이다음 패킷의 시퀀스 번호를 확인해 패킷의 누락을 확인한다.

아래 그림처럼 이전 데이터의 시퀀스 번호가 0이고, 데이터 크기가 10일 경우, 그 다음 패킷의 시퀀스번호가 10이여야 패킷이 누락되지 않은 것이다.

만일 다음 패킷의 시퀀스 번호가 20처럼 연속되지 않을 경우, 패킷이 누락되었음을 확인할 수 있다.

만일 수신측에서 정상적으로 수신했을 경우, TCP 헤더에 ACK 번호에 이전에 수신한 데이터와 합친 바이트 값을 기록한다.

ACK까지 되돌려 주는 동작을 수신 확인 응답이라고 한다.

실제로는 시퀀스 번호가 1부터 시작하지 않는고 난수를 바탕으로 산출한 초기값으로 시작한다. 악의적인 공격을 할 우려가 있어서다. 처음 SYN 제어 비트를 1로 하는 부분이 있었는데, 그 부분이 초기값을 통지하는 것이다.

실제로는 서버 → 클라이언트, 클라이언트 → 서버로 보내는 양방향 데이터 흐름이 있기 때문에, 서버와 클라어인트 각각 시퀀스 번호를 산출해 데이터와 함께 보내고, ACK 번호를 받는다.

그림으로 보면 아래와 같다.

TCP는 상대가 데이터 받는 것을 확인하기 위해 송신용 버퍼 메모리 영역에 송신한 패킷을 보관해둔다. 만일 송신한 데이터에 대응하는 ACK 번호가 상대로부터 돌아오지 않으면 패킷을 다시 보낸다.

이러한 구조 덕분에, 다른 곳에서 오류 회복 조치를 할 필요가 없어졌다.

만일 케이블 분리, 서버 다운 등의 이유로 데이터를 보내도 데이터가 도착하지 않는 경우, 한없이 다시 보내지 않기 위해 데이터 송신 동작을 종료하고 애플리케이션 오류를 통지한다.

4 패킷 완복 시간으로 ACK 번호의 대기 시간을 조정한다.

ACK 번호가 돌아오는데 걸리는 시간을 타임아웃 값이라고 한다.

만일 네트워크 혼잡으로 정체가 발생해 ACK 번호가 돌아오는데 지연이 되므로 이것을 예측하여 대기 시간을 길게 설정해야한다. 안그러면 ACK 번호가 돌아오기도 전에 패킷을 다시 보내는 사태가 발생.

하지만 이 대기 시간을 예측하는 작업은 어렵다. 서버와의 거리와 정체 정도 등 여러 변수에 따라 대기 시간이 다르기 때문이다.

이 때문에 TCP는 대기 시간을 동적으로 변경하는 방법을 사용한다.

즉 ACK 번호가 돌아오는 시간을 기준으로 대기 시간을 판단한다. 만일 ACK 번호가 돌아오는데 시간이 걸린다면 이것에 대응해 대기 시간을 늘리고, 곧바로 돌아온다면 대기 시간을 짧게 설정한다.

5 윈도우 제어 방식

위의 방법은 패킷을 보내고 ACK 번호가 도착할때까지 기다리는 시간 동안 아무것도 하지 않기 때문에 낭비가 발생한다.

따라서 한 개의 패킷을 보낸 후 ACK 번호를 기다리지 않고 차례대로 연속해서 복수의 패킷을 보내는 방법을 사용한다.

다만 이렇게 보낼 경우, 수신측의 처리가 끝나지 않았는데 패킷을 받으므로, 잘못하면 수신측의 처리 능력을 벋어나 처리 못하는 경우가 생긴다.

여기서 처리 능력이 벗어났다는 의미는 송신측으로부터 패킷이 오는 속도가 수신측의 데이터 처리 속도보다 빨라 수신 버퍼에 데이터가 넘친다는 의미이다.

이런 경우를 피하기 위해 윈도우 제어 방식을 사용한다.

윈도우 제어방식은 간단히 얘기하면, 수신측의 수신할 수 있는 데이터 크기를 TCP 헤더의 윈도우 필드에 기록해 송신측에 알리는 것이다.

여기서 수신 가능한 데이터의 최대값을 **윈도우 사이즈(보통 수신측 버퍼 메모리 크기)**라고 한다.

6 ACK 번호와 윈도우 통지 합치기

윈도우 통지의 동작은 언제 동작할까?

윈도우 통지는 수신측에서 버퍼에 있는 데이터를 애플리케이션에 건내주어 처리했을 경우에 발생한다.

ACK 번호 통지패킷을 받을때마다 동작한다.

둘이 따로 패킷을 보낼 경우, 보내는 패킷이 많아져 효율성이 저하된다.

따라서 이 둘을 요리조리 잘 섞어서 보낼 수 있다면 효율성이 증가할 것이다.

어떻게 할까? 두 가지 동작이 필요하다.

  1. 기다리기
  2. 생략하기

기다리기

기다리는 동작은 서로의 통지를 기다려 두 통지 모두 보낼 경우 한 패킷에 묶어 보내는 것이다.

생략하기

생략하기 동작은 기다리는 동안 통지가 연속해서 일어날 경우 중간 통지를 생략하여 최후의 통지만 하는 것이다.

ACK 번호의 경우 번호의 끝을 알리는 것이기 때문에 연속해서 발생하더라도 최후의 번호만 통지하면 된다.

윈도우 통지도 마찬가지로 통지가 연속해서 발생한다는 것은 버퍼 공간이 연속해서 늘어난다는 것이고 따라서 최후의 버퍼 공간만 통지하면 된다.

7 HTTP 응답 메세지 수신

이제 보낸 데이터를 수신하기 위한 과정이다.

브라우저는 서버에서 돌아오는 응답 메세지를 받기 위해 read()를 호출한다.

read()를 호출할 경우, 제어 주도권은 프로토콜 스택이 가져간다.

프로토콜 스택은 만약 수신 버퍼에 수신된 데이터가 없다면 쉰다.(쉰다고 하더라도 다른 작업을 한다는 의미이다.)

그러고 서버에서 응답 메세지 패킷이 돌아올 경우 그것을 수신해 애플리케이션에게 넘겨준다.